# D:\project\202509\mediapipe-jump-rope\src\visualize_raw_data.py

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import argparse
from pathlib import Path
from scipy.signal import find_peaks, butter, lfilter
from scipy.ndimage import binary_opening, binary_closing

def find_active_segment_indices_v_champion(df, fs=30.0, min_len_secs=3.0, output_path=None):
    """
    冠军版活跃期识别：100%恢复您验证过的、最有效的版本。
    """
    # ############## 您的最佳参数 ##############
    high_quantile = 0.80
    low_quantile = 0.05
    # ######################################
    
    if 'pose_bbox_height' not in df.columns or df['pose_bbox_height'].isnull().all():
        if 'left_ankle_y' not in df.columns or 'right_ankle_y' not in df.columns: return df.index.min(), df.index.max()
        signal_source = pd.Series(np.abs(df['left_ankle_y'].diff()) + np.abs(df['right_ankle_y'].diff()), index=df.index)
        print("信息: 使用'脚踝速度'作为活跃期判断依据。")
    else:
        signal_source = df['pose_bbox_height']
        print("信息: 使用'身体包围盒'作为活跃期判断依据。")
        
    def butter_highpass(cutoff, fs, order=5):
        b, a = butter(order, cutoff / (0.5 * fs), btype='high', analog=False); return b, a
    def highpass_filter(data, cutoff=2.0, fs=fs, order=5):
        b, a = butter_highpass(cutoff, fs, order=order); return lfilter(b, a, data)
        
    hp_signal = highpass_filter(signal_source.interpolate().fillna(0), fs=fs)
    energy = pd.Series(hp_signal, index=df.index).abs().rolling(window=int(fs), center=True, min_periods=1).mean()
    
    valid_energy = energy.dropna()
    if len(valid_energy) < 20: return df.index.min(), df.index.max()
        
    high_threshold = valid_energy.quantile(high_quantile)
    low_threshold = valid_energy.quantile(low_quantile)
    
    is_active = False; active_segments = []; start_idx = None
    for i, e in energy.items():
        if not is_active and e > high_threshold:
            is_active = True
            start_idx = i
        elif is_active and e < low_threshold:
            is_active = False
            if start_idx is not None:
                if (i - 1 - start_idx) >= (min_len_secs * fs):
                    active_segments.append((start_idx, i - 1))
                start_idx = None
    if start_idx is not None and (energy.index[-1] - start_idx) >= (min_len_secs * fs):
        active_segments.append((start_idx, energy.index[-1]))
        
    if output_path:
        plt.figure(figsize=(max(20, len(df)/100), 6))
        plt.plot(energy.index, energy, label='能量曲线')
        plt.axhline(y=high_threshold, color='red', linestyle='--', label=f'高门槛 ({high_threshold:.4f})')
        plt.axhline(y=low_threshold, color='green', linestyle='--', label=f'低门槛 ({low_threshold:.4f})')
        for i, (start, end) in enumerate(active_segments):
            plt.axvspan(start, end, color='gold', alpha=0.3, label=f'识别区_{i+1}' if i == 0 else "")
        plt.title('活跃期识别 - 调试视图'); plt.xlabel('帧号'); plt.ylabel('能量')
        handles, labels = plt.gca().get_legend_handles_labels(); by_label = dict(zip(labels, handles))
        plt.legend(by_label.values(), by_label.keys()); plt.grid(True)
        debug_path = str(output_path).replace('.png', '_debug_energy.png')
        plt.savefig(debug_path); plt.close()
        print(f"调试图表已保存到: {debug_path}")

    if not active_segments: return df.index.min(), df.index.max()
    
    longest_segment = max(active_segments, key=lambda s: s[1] - s[0])
    return longest_segment[0], longest_segment[1]

def calculate_independent_drifting_rails(data_series, active_start, active_end, params):
    # ... (这个函数保持不变)
    active_data = data_series.loc[active_start:active_end].dropna()
    if len(active_data) < 50: return None, None
    discovery_distance = 3
    peaks_idx, _ = find_peaks(active_data, distance=discovery_distance, prominence=active_data.std() * 0.2)
    valleys_idx, _ = find_peaks(-active_data, distance=discovery_distance, prominence=active_data.std() * 0.2)
    if len(peaks_idx) < 5 or len(valleys_idx) < 5: return None, None
    peak_values = active_data.iloc[peaks_idx]; valley_values = active_data.iloc[valleys_idx]
    trim_percent = params['trim_percent']; lower_q = (1 - trim_percent) / 2; upper_q = 1 - lower_q
    elite_peaks = peak_values[(peak_values >= peak_values.quantile(lower_q)) & (peak_values <= peak_values.quantile(upper_q))]
    elite_valleys = valley_values[(valley_values >= valley_values.quantile(lower_q)) & (valley_values <= valley_values.quantile(upper_q))]
    golden_upper_level = elite_peaks.median(); golden_lower_level = elite_valleys.median()
    active_center = active_data.median()
    drifting_center = data_series.loc[active_start:active_end].rolling(window=params['smooth_window'], center=True, min_periods=1).mean().reindex(data_series.index)
    upper_offset = golden_upper_level - active_center; lower_offset = golden_lower_level - active_center
    final_upper_rail = drifting_center + upper_offset; final_lower_rail = drifting_center + lower_offset
    golden_amplitude = golden_upper_level - golden_lower_level
    buffer = (golden_amplitude / 2) * params['tolerance_ratio']
    return final_upper_rail + buffer, final_lower_rail - buffer

def get_ankle_amplitudes_and_threshold(data_series, active_start, active_end, params):
    # (为脚踝计算振幅和达标线)
    active_data = data_series.loc[active_start:active_end].dropna()
    if len(active_data) < 50: return None, None
    discovery_distance = 3
    peaks_idx, _ = find_peaks(active_data, distance=discovery_distance, prominence=active_data.std() * 0.2)
    valleys_idx, _ = find_peaks(-active_data, distance=discovery_distance, prominence=active_data.std() * 0.2)
    if len(peaks_idx) < 5 or len(valleys_idx) < 5: return None, None
    all_extrema_idx = sorted(np.concatenate([peaks_idx, valleys_idx]))

    # --- 1. 初步扫描，收集所有半周期时长 ---
    all_durations = []
    for i in range(len(all_extrema_idx) - 1):
        frame1 = active_data.index[all_extrema_idx[i]]
        frame2 = active_data.index[all_extrema_idx[i+1]]
        all_durations.append(frame2 - frame1)
    
    if not all_durations: return None, None

    # --- 2. 计算黄金节奏并设定动态阈值 ---
    durations_series = pd.Series(all_durations)
    # 移除极端异常值，比如只保留10%到90%分位数之间的时长
    q10 = durations_series.quantile(0.10)
    q90 = durations_series.quantile(0.90)
    elite_durations = durations_series[(durations_series >= q10) & (durations_series <= q90)]
    
    if elite_durations.empty:
        golden_duration = durations_series.mean() # 回退到使用普通平均值
    else:
        golden_duration = elite_durations.mean()

    # 动态设定最大允许时长，乘以2作为容忍系数，并设置一个绝对上限(30帧)防止极端情况
    max_jump_duration_frames = min(int(golden_duration * 2.0), 30)
    print(f"信息: {data_series.name} - 黄金跳跃半周期: {golden_duration:.2f} 帧, 动态时长上限: {max_jump_duration_frames} 帧")

    # --- 3. 带动态阈值，正式计算振幅 ---
    amplitudes = []
    amplitude_frames = []
    for i in range(len(all_extrema_idx) - 1):
        frame1 = active_data.index[all_extrema_idx[i]]
        frame2 = active_data.index[all_extrema_idx[i+1]]
        duration = frame2 - frame1

        if duration > max_jump_duration_frames:
            continue # 使用动态阈值进行过滤

        amp = abs(data_series[frame2] - data_series[frame1])
        amplitudes.append(amp)
        amplitude_frames.append(int((frame1 + frame2) / 2))

    if not amplitudes: return None, None
    amplitudes = pd.Series(amplitudes, index=amplitude_frames)
    
    # ... (后续计算 elite_amplitudes 等代码保持不变) ...
    trim_percent = params['trim_percent']; lower_q = (1 - trim_percent) / 2; upper_q = 1 - lower_q
    elite_amplitudes = amplitudes[(amplitudes >= amplitudes.quantile(lower_q)) & (amplitudes <= amplitudes.quantile(upper_q))]
    golden_amplitude = elite_amplitudes.median()
    min_amplitude_threshold = golden_amplitude * params['min_amplitude_ratio']
    return amplitudes, min_amplitude_threshold, golden_amplitude, golden_duration

def morphological_post_processing(is_normal_series, open_size=3, close_size=11):
    # ... (这个函数保持不变)
    opening_structure = np.ones(open_size); closing_structure = np.ones(close_size)
    opened_series = binary_opening(is_normal_series.values, structure=opening_structure)
    closed_series = binary_closing(opened_series, structure=closing_structure)
    return pd.Series(closed_series, index=is_normal_series.index)
    
def validate_signal_rhythm_with_debug(data_series, golden_duration, tolerance=0.50, fs=30.0):
    """
    【V5 - 终极调试版】
    返回详细的调试信息，以便在图表上可视化算法的决策过程。
    """
    debug_info = {}
    
    if data_series.isnull().all() or len(data_series.dropna()) < 20:
        return pd.Series(True, index=data_series.index), debug_info

    # 找到所有可能的波峰和波谷
    peaks_idx, _ = find_peaks(data_series, distance=3, prominence=data_series.std() * 0.05)
    valleys_idx, _ = find_peaks(-data_series, distance=3, prominence=data_series.std() * 0.05)
    
    # 存储找到的极值点以供调试
    debug_info['found_peaks'] = data_series.iloc[peaks_idx]
    debug_info['found_valleys'] = data_series.iloc[valleys_idx]
    
    all_extrema_idx = sorted(np.concatenate([peaks_idx, valleys_idx]))
    
    if len(all_extrema_idx) < 3:
        return pd.Series(True, index=data_series.index), debug_info

    extrema_frames = data_series.index[all_extrema_idx]
    half_period_intervals = np.diff(extrema_frames)
    interval_midpoints = extrema_frames[:-1] + half_period_intervals / 2

    lower_bound = golden_duration * (1 - tolerance)
    upper_bound = golden_duration * (1 + tolerance)
    is_rhythmic_interval = (half_period_intervals >= lower_bound) & (half_period_intervals <= upper_bound)
    
    rhythm_quality = pd.Series(is_rhythmic_interval.astype(int), index=interval_midpoints)
    
    window_size = 5 
    local_quality_score = rhythm_quality.rolling(window=window_size, center=True, min_periods=1).mean()
    
    expanded_score = local_quality_score.reindex(data_series.index).bfill().ffill().fillna(0)
    
    # 存储质量得分和阈值
    debug_info['quality_score'] = expanded_score
    score_threshold = 0.6
    debug_info['score_threshold'] = score_threshold

    rhythm_ok = expanded_score > score_threshold
    
    return rhythm_ok, debug_info

def visualize_jump_rope_raw_data_final(csv_path, output_path):
    csv_path = Path(csv_path)
    if not csv_path.exists(): return
    print(f"正在读取和可视化数据 (最终版): {csv_path}")
    df = pd.read_csv(csv_path)
    try: fs = 1 / (df['timestamp'].diff().mean())
    except: fs = 30.0

    valid_frames = df[df['has_pose'] == True].set_index('frame_number')
    if len(valid_frames) < 100: return

    frame_indices = valid_frames.index.values
    plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
    plt.rcParams['axes.unicode_minus'] = False
    
    num_frames = len(frame_indices)
    # 核心修改：将宽度系数从 100 减小到 50，相当于将X轴放大一倍
    width_inches = max(20, num_frames / 50.0) 
    height_inches_per_plot = 3.5; total_height_inches = height_inches_per_plot * 8
    fig, axes = plt.subplots(8, 1, figsize=(width_inches, total_height_inches), sharex=True)
    
    PARAMS = {
        'default': {'trim_percent': 0.8,'smooth_window': 51},
        'wrist': {'tolerance_ratio': 2.0},
        'ankle': {'tolerance_ratio': 2.0, 'min_amplitude_ratio': 0.5},
        'com': {'tolerance_ratio': 2.0},
        'post_processing': {'open_size': 5, 'close_size': 11}
    }

    # ############## 核心改动：调用您验证过的冠军版活跃期识别函数 ##############
    # active_start, active_end = find_active_segment_indices_v_champion(valid_frames, fs=fs, output_path=output_path)
    active_start = valid_frames.index.min()
    active_end = valid_frames.index.max()
    print(f"模式更新: 使用全视频范围进行可视化分析: 帧 {active_start} 到 {active_end}")
    # ##############################################################
    
    print(f"自动识别的总活跃期: 帧 {active_start} 到 {active_end}")
    
    is_normal_df = pd.DataFrame(index=valid_frames.index, dtype=bool)
    plot_map = {
        1: {'data': valid_frames['center_of_mass_y'], 'title': '重心 (Y)', 'color': '#d62728', 'type': 'com'},
        2: {'data': valid_frames['left_wrist_y'], 'title': '左手腕 (Y)', 'color': '#ff7f0e', 'type': 'wrist'},
        3: {'data': valid_frames['right_wrist_y'], 'title': '右手腕 (Y)', 'color': '#f7b6d2', 'type': 'wrist'},
        4: {'data': valid_frames['left_ankle_y'], 'title': '左脚踝 (Y)', 'color': '#2ca02c', 'type': 'ankle'},
        6: {'data': valid_frames['right_ankle_y'], 'title': '右脚踝 (Y)', 'color': '#98df8a', 'type': 'ankle'}
    }
    
    golden_durations = {}
    rail_data_map = {} 
    # ... (后续所有代码与V42.1版本完全相同)
    for ax_idx, props in plot_map.items():
        ax = axes[ax_idx]; ax.set_title(props['title'])
        data_series = props['data']; ax.plot(frame_indices, data_series, color=props['color'], linewidth=1)
        part_type = props['type']; part_params = {**PARAMS['default'], **PARAMS.get(part_type, {})}
        up, low = calculate_independent_drifting_rails(data_series, active_start, active_end, part_params)
        if up is not None:
            rail_data_map[ax_idx] = {'up': up, 'low': low}
            ax.plot(frame_indices, up, color='red', linestyle='-', linewidth=2, label='黄金轨道')
            ax.plot(frame_indices, low, color='red', linestyle='-', linewidth=2)
            is_normal_raw = (data_series >= low) & (data_series <= up)
            is_normal_processed = morphological_post_processing(is_normal_raw, **PARAMS['post_processing'])
            is_normal_df[props['title']] = is_normal_processed
            
            # normal_segments_indiv = []
            # start_idx_indiv = None
            # for frame, normal in is_normal_processed.loc[active_start:active_end].items():
            #     if normal and start_idx_indiv is None: start_idx_indiv = frame
            #     elif not normal and start_idx_indiv is not None:
            #         normal_segments_indiv.append((start_idx_indiv, frame - 1)); start_idx_indiv = None
            # if start_idx_indiv is not None: normal_segments_indiv.append((start_idx_indiv, is_normal_processed.index[-1]))
            
            # for seg_start, seg_end in normal_segments_indiv:
            #     ax.axvspan(seg_start, seg_end, color='lightgreen', alpha=0.4, zorder=0)
        # ax.axvspan(active_start, active_end, color='gray', alpha=0.1, zorder=-1)
    
    for ax_amp_idx, data_series, color, title in [(5, valid_frames['left_ankle_y'], '#2ca02c', '左脚踝'), (7, valid_frames['right_ankle_y'], '#98df8a', '右脚踝')]:
        ax = axes[ax_amp_idx]; part_params = {**PARAMS['default'], **PARAMS['ankle']}
        amplitudes, threshold, golden_amp, golden_duration = get_ankle_amplitudes_and_threshold(data_series, active_start, active_end, part_params)
        
        if golden_duration: # <--- 新增
            golden_durations[data_series.name] = golden_duration
        is_normal_df[f'{title} 振幅达标'] = False
        if amplitudes is not None:
            ax.bar(amplitudes.index, amplitudes.values, width=3, color=color); ax.axhline(y=threshold, color='red', linestyle='--', linewidth=2, label=f'达标线 ({threshold:.3f})')
            ax.set_title(f"{title} 振幅 (黄金: {golden_amp:.3f})")
            # --- 核心修改在这里 ---
            if golden_duration and golden_duration > 0:
                half_limit = int(np.clip(golden_duration * 1.2, 3, 12))
            else:
                half_limit = 5 
            
            amp_ok = (amplitudes >= threshold).reindex(valid_frames.index) \
                                              .bfill(limit=half_limit) \
                                              .ffill(limit=half_limit) \
                                              .fillna(False)
            # ---------------------
            
            is_normal_df[f'{title} 振幅达标'] = morphological_post_processing(amp_ok, **PARAMS['post_processing'])

    is_normal_df.fillna(True, inplace=True)

    # --- 核心修改：将脚踝振幅的 AND 判断改为 OR 判断 ---
    left_amp_col = '左脚踝 振幅达标'
    right_amp_col = '右脚踝 振幅达标'
    if left_amp_col in is_normal_df.columns and right_amp_col in is_normal_df.columns:
        is_normal_df['any_ankle_amp_ok'] = is_normal_df[left_amp_col] | is_normal_df[right_amp_col]
        is_normal_df.drop(columns=[left_amp_col, right_amp_col], inplace=True)
    # --- 修改结束 ---

    if golden_durations:
        base_duration = min(golden_durations.values())
        active_data = valid_frames
        
        wrist_map = {'左手腕 (Y)': 'left_wrist_y', '右手腕 (Y)': 'right_wrist_y'}
        
        for ax_idx, (title, col_name) in enumerate(wrist_map.items(), start=2):
            if col_name not in active_data.columns: continue
            
            ax = axes[ax_idx] # 获取对应手腕的子图
            data_series = active_data[col_name]
            
            # 调用新的带调试功能的函数
            rhythm_ok, debug_info = validate_signal_rhythm_with_debug(
                data_series, golden_duration=base_duration, tolerance=0.50, fs=fs
            )
            
            # 将节律判定结果加入is_normal_df
            is_normal_df[title + '节律'] = morphological_post_processing(rhythm_ok, open_size=3, close_size=7)

            # --- 核心：在这里将调试信息可视化！ ---
            if debug_info:
                # 1. 绘制找到的波峰和波谷
                ax.scatter(debug_info['found_peaks'].index, debug_info['found_peaks'].values, 
                           facecolors='none', edgecolors='cyan', marker='o', s=20, 
                           label='找到的波峰', zorder=5, linewidths=1.5)
                ax.scatter(debug_info['found_valleys'].index, debug_info['found_valleys'].values, 
                           color='magenta', marker='x', s=20, 
                           label='找到的波谷', zorder=5, alpha=0.7, linewidths=1.5)
                
                # 2. 绘制节律质量得分曲线
                ax2 = ax.twinx() # 创建共享x轴的第二个y轴
                ax2.plot(debug_info['quality_score'].index, debug_info['quality_score'], 
                         color='blue', linestyle=':', linewidth=1.5, label='节律质量分')
                ax2.axhline(y=debug_info['score_threshold'], color='blue', linestyle='--', 
                            linewidth=1.5, label=f'及格线 ({debug_info["score_threshold"]})')
                ax2.set_ylim(0, 1.1)
                ax2.set_ylabel('质量分', color='blue')
                
                # 合并图例
                lines, labels = ax.get_legend_handles_labels()
                lines2, labels2 = ax2.get_legend_handles_labels()
                ax2.legend(lines + lines2, labels + labels2, loc='upper right')
    
    final_normal_mask = is_normal_df.all(axis=1)
    
    plot_ax_map = {1: '重心 (Y)', 2: '左手腕 (Y)', 3: '右手腕 (Y)', 4: '左脚踝 (Y)', 6: '右脚踝 (Y)'}
    ankle_amp_cols = ['左脚踝 振幅达标', '右脚踝 振幅达標'] # 修正可能的拼写错误
    wrist_rhythm_cols = ['左手腕 (Y)节律', '右手腕 (Y)节律']

    for ax_idx, col_base_name in plot_ax_map.items():
        ax = axes[ax_idx]
        
        # 构建当前分图依赖的所有验证列
        dependent_cols = [col_base_name]
        if '手腕' in col_base_name:
            dependent_cols.extend(wrist_rhythm_cols)
        if '脚踝' in col_base_name:
            # 对于脚踝，我们依赖于合并后的 'any_ankle_amp_ok'
             if 'any_ankle_amp_ok' in is_normal_df.columns:
                dependent_cols.append('any_ankle_amp_ok')
        
        # 只选择存在的列进行 AND 操作
        valid_dependent_cols = [col for col in dependent_cols if col in is_normal_df.columns]
        if not valid_dependent_cols: continue

        individual_mask = is_normal_df[valid_dependent_cols].all(axis=1)
        
        # 根据这个独立的mask来绘制绿色背景
        start_idx_indiv = None
        for frame, normal in individual_mask.items():
            if normal and start_idx_indiv is None: start_idx_indiv = frame
            elif not normal and start_idx_indiv is not None:
                ax.axvspan(start_idx_indiv, frame - 1, color='lightgreen', alpha=0.4, zorder=0)
                start_idx_indiv = None
        if start_idx_indiv is not None:
            ax.axvspan(start_idx_indiv, individual_mask.index[-1], color='lightgreen', alpha=0.4, zorder=0)

    final_jumps = []
    normal_segments = []
    start_idx = None
    if golden_durations:
        # 使用最快的节奏（最小的duration）作为基准
        base_duration = min(golden_durations.values())
        # 最小长度为一个半完整跳跃周期，并设置下限为5帧
        min_segment_len = max(5, int((base_duration * 2) * 1.5))
    else:
        min_segment_len = 10 # 回退到旧的硬编码值
    
    print(f"信息: 动态最小正常区长度: {min_segment_len} 帧")
    # --- 修改结束 ---

    for frame, normal in final_normal_mask.items():
        if normal and start_idx is None: start_idx = frame
        elif not normal and start_idx is not None:
            if frame - 1 - start_idx > min_segment_len: # <--- 使用动态值
                normal_segments.append((start_idx, frame - 1))
            start_idx = None
    if start_idx is not None and (final_normal_mask.index[-1] - start_idx) > min_segment_len: # <--- 使用动态值
        normal_segments.append((start_idx, final_normal_mask.index[-1]))
    
    print(f"在活跃期内识别出 {len(normal_segments)} 个最终正常区: {normal_segments}")
    
    for seg_start, seg_end in normal_segments:
        seg_data_l = valid_frames.loc[seg_start:seg_end, 'left_ankle_y']; seg_data_r = valid_frames.loc[seg_start:seg_end, 'right_ankle_y']
        if len(seg_data_l) < 5: continue
        l_g_dur = golden_durations.get('left_ankle_y', 5)
        r_g_dur = golden_durations.get('right_ankle_y', 5)

        l_dist = max(3, int((l_g_dur * 2) * 0.7))
        r_dist = max(3, int((r_g_dur * 2) * 0.7))
        
        peak_prom = seg_data_l.std() * 0.20

        l_peaks, _ = find_peaks(-seg_data_l, distance=l_dist, prominence=peak_prom)
        r_peaks, _ = find_peaks(-seg_data_r, distance=r_dist, prominence=peak_prom)
        seg_jumps = sorted(list(set(np.concatenate([seg_data_l.index[l_peaks], seg_data_r.index[r_peaks]]))))
        if not seg_jumps: continue
        merged_jumps = [seg_jumps[0]]
        for i in range(1, len(seg_jumps)):
            if seg_jumps[i] - merged_jumps[-1] > 3: merged_jumps.append(seg_jumps[i])
        final_jumps.extend(merged_jumps)
        
    video_name = Path(csv_path).stem.replace('_frame_data', ''); fig.suptitle(f'跳绳数据验证可视化 (最终计数: {len(final_jumps)}): {video_name}', fontsize=24, fontweight='bold')
    
    axes[0].set_title('合并数据视图 (全局判定)', fontsize=18)
    merge_map = {**plot_map, 5: {'data': valid_frames['right_ankle_y'], 'color': '#98df8a'}}
    for _, props in merge_map.items():
        y_data_raw = props['data'].dropna()
        if not y_data_raw.empty:
            y_data_scaled = (y_data_raw - y_data_raw.min()) / ((y_data_raw.max() - y_data_raw.min()) or 1)
            axes[0].plot(y_data_scaled.index, y_data_scaled, color=props['color'])
    axes[0].axvspan(active_start, active_end, color='gray', alpha=0.1, label='总活跃期')
    for seg_start, seg_end in normal_segments:
        axes[0].axvspan(seg_start, seg_end, color='gold', alpha=0.5, label='最终正常区')
    
    for i in range(8):
        ax = axes[i]
        
        # --- 在循环开始时获取一次轨道数据 ---
        is_y_coord_plot = i in [1, 2, 3, 4, 6]
        up, low = None, None # 先初始化
        if is_y_coord_plot:
            rails = rail_data_map.get(i)
            if rails:
                up = rails['up']
                low = rails['low']
        # ------------------------------------

        for jump_idx, frame in enumerate(final_jumps):
            is_amp_plot = i in [5, 7]
            is_merge_plot = i == 0
            
            if is_amp_plot:
                ax.axvline(x=frame, color='blue', linestyle=':', linewidth=0.8, alpha=0.7)
                continue

            # --- 核心修改：新的 ax.text 绘图逻辑 ---
            # 确定锚定轨道：偶数在上轨，奇数在下轨
            rail_series = up if jump_idx % 2 == 0 else low
            
            # 检查轨道数据是否存在于当前帧
            if rail_series is not None and frame in rail_series.index:
                y_rail_anchor = rail_series.loc[frame]
                
                # 在轨道基础上再做一点微小偏移，避免压线
                y_range = ax.get_ylim()
                # 注意：因为Y轴是反的，所以计算offset时用 (y_range[0] - y_range[1])
                offset = (y_range[0] - y_range[1]) * 0.12 
                
                # 偶数在上轨(更小的值)，所以要减去offset；奇数在下轨(更大的值)，所以要加上offset
                y_final_pos = y_rail_anchor - offset if jump_idx % 2 == 0 else y_rail_anchor + offset

                ax.text(frame, y_final_pos, str(jump_idx + 1), color='blue', fontsize=8, 
                        ha='center', va='center', 
                        bbox=dict(boxstyle="round,pad=0.15", facecolor='white', alpha=0.75, edgecolor='none'))
            else: 
                # 如果轨道不存在（例如在合并图或振幅图中），使用旧的回退逻辑
                y_anchor = 0 # 初始化
                if is_y_coord_plot and frame in plot_map[i]['data'].index:
                    y_anchor = plot_map[i]['data'].loc[frame]
                elif is_merge_plot and frame in valid_frames.index:
                    y_anchor_raw = (valid_frames['left_ankle_y'].loc[frame] + valid_frames['right_ankle_y'].loc[frame]) / 2
                    y_min = valid_frames[['left_ankle_y', 'right_ankle_y']].min().min()
                    y_max = valid_frames[['left_ankle_y', 'right_ankle_y']].max().max()
                    y_anchor = (y_anchor_raw - y_min) / ((y_max - y_min) or 1)
                
                if pd.notna(y_anchor) and y_anchor != 0:
                    offset_direction = 1 if jump_idx % 2 == 0 else -1
                    y_range = ax.get_ylim(); 
                    offset = (y_range[0] - y_range[1]) * 0.12 * offset_direction
                    ax.text(frame, y_anchor + offset, str(jump_idx + 1), color='blue', fontsize=8, 
                            ha='center', va='center', 
                            bbox=dict(boxstyle="round,pad=0.15", facecolor='white', alpha=0.75, edgecolor='none'))
                    
    for ax in axes:
        if ax not in [axes[5], axes[7]]: ax.invert_yaxis()
        handles, labels = ax.get_legend_handles_labels(); by_label = dict(zip(labels, handles))
        ax.legend(by_label.values(), by_label.keys(), loc='upper right')
        ax.grid(True, linestyle='--', alpha=0.6); ax.set_ylabel('Y坐标/振幅')
    plt.xlabel('帧号 (Frame Number)'); plt.tight_layout(rect=[0, 0, 1, 0.96])
    plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white'); plt.close()
    
    print(f"\n✅ V_Final_Debug 版可视化图表已生成！")
    print(f"图表已保存到: {output_path}")

def main():
    parser = argparse.ArgumentParser(description='为跳绳CSV数据生成最终版可视化图表 (带调试日志)。')
    parser.add_argument('--csv', required=True, help='CSV文件路径。')
    parser.add_argument('--output', help='输出图表图片的路径 (可选)。')
    args = parser.parse_args()
    csv_path = Path(args.csv)
    if args.output: output_path = Path(args.output)
    else: output_path = csv_path.parent / f"{csv_path.stem}_raw_visualization_final_debug.png"
    visualize_jump_rope_raw_data_final(csv_path, output_path)

if __name__ == "__main__":
    main()